/*----------------------------------------------------------------------------*\
| /gamemode/inventory.lua              | inventory system                | SRV |
\*----------------------------------------------------------------------------*/

-- Define Inventory table.
local INV = {}

-- Some basic configuration.
INV.Conf = {}
INV.Conf.NumSlots 		= 20 	-- The maximum number of objects.
INV.Conf.SlotWeight 	= 100 	-- The maximum weight each object can be.
INV.Conf.MaxWeight		= 400	-- Maximum weight anyone can hold.

function INV.Init()
	local tab = ents.FindByClass( "prop_physics" )
	//print("IT'S KIND OF WORKING")
	//table.Add(tab,ents.FindByName( "prop_physics_multiplayer" ))
	for index, ent in pairs( tab ) do
		ent:SetMaxHealth(ent:GetPhysicsObject():GetMass() * 2)
		ent:SetHealth(ent:GetPhysicsObject():GetMass() * 2)
		//print(ent:Health() .. "/" .. ent:GetMaxHealth())
	end
end
hook.Add( "InitPostEntity", "INV.Init", function() INV.Init() end )

-- Initalize everything for each player when they spawn.
function INV.PlayerInit( ply )
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.
	
	-- We will have a table for all the actual objects stored.
	INV[UID]				= {}
	INV[UID].TotalWeight 	= 0
	INV[UID].Objects 		= {}
	INV[UID].Held			= nil
	INV[UID].HeldAngle		= Angle(0,0,0)
	INV[UID].HeldOther		= nil
	
	for i = 1, INV.Conf.NumSlots do	-- Lets do this properly this time, with 1 indexed tables.
		INV[UID].Objects[i] = {}
		INV.ResetObject( UID, i )
	end
end
hook.Add( "PlayerInitialSpawn", "INV.PlayerInit", INV.PlayerInit )

-- De-Initalize everything for each player as they leave.
function INV.DeInit( ply )
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.
	INV[UID] = nil
end
hook.Add( "PlayerDisconnected", "INV.DeInit", INV.DeInit )

-- Helper functions and stuff.
function INV.ResetObject( UID, id )
	local obj = INV[UID].Objects[id]
	obj.InUse		= false
	obj.Entity 		= nil
	obj.Weight		= 0
	obj.Name		= 0
	obj.Model		= 0
end 

-- Takes a table of slots to update.
function INV.UpdateClient( UID, id )
	local obj = INV[UID].Objects[id]

	umsg.Start( "InventoryUpdate", ply )
		umsg.Short( id )
		umsg.Bool( obj.InUse )		
		umsg.Short( obj.Weight )
		umsg.String( tostring( obj.Name ) )
		umsg.String( tostring( obj.Model ) )		
	umsg.End()
end 

-- Basic checks to stop people picking up what they should not.
function INV.CanPickUp( ent, UID )
	-- First up check that we are not trying to pick up a player.
	if ent:IsPlayer() then
		return false
	end
	
	-- Then check out the classes.
	local class = ent:GetClass()
	if string.find( ent:GetClass(),  "npc_" ) then
		return false -- We can't pick up NPC's.
	elseif string.find( ent:GetClass(), "prop_generator" ) then
		return false -- We can't pick up Generators either.
	elseif string.find( ent:GetClass(), "ammo" ) then
		return false // nope, no ammo pickups either.
	elseif string.find( ent:GetClass(), "door" ) then
		return false // lol doors
	elseif string.find( ent:GetName(), "NOPICKUP" ) then
		return false
	elseif ent.owner and ent.owner.ragdolled then
		return false
	end
	-- TODO: Add more "non-pick-up-ables".
	
	-- Check the weights.
	local physobj = ent:GetPhysicsObject()		-- Grab the entity's physobject
	//if !physobj || !physobj:IsValid() then return false end
	
	-- Grab the entitys weight and then calculate the required number of slots needed to hold it.
	local weight = math.floor( physobj:GetMass() )
	
	if !weight || weight > INV.Conf.SlotWeight || INV[UID].TotalWeight + weight > INV.Conf.MaxWeight then
		return false
	end
	
	-- Everything appears fine, continue.
	return true	
end

-- Function to grab the first empty slot or return false if none.
function INV.GetSlot( UID )
	for k, v in pairs( INV[UID].Objects ) do
		if !v.InUse then
			return k
		end
	end
	return false
end

-- Pick up function, called by key press hook.
function INV.PickUp( ply, ent )
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.

	-- Double check that the entity exists and is valid.
	if !ent || !ent:IsValid() then
		return false
	end
	
	-- Early out if the CanPickUp function returns false.
	if !INV.CanPickUp( ent, UID ) then
		return false
	end

	local id = INV.GetSlot( UID )
	if !id then
		return false
	end
	
	local obj = INV[UID].Objects[id]
	
	-- Save ( a reference to ) the entity as well as the slots list for the object. 
	obj.Entity = ent
	
	-- If something stuffs up, remove what we have done and end.
	if !obj.Entity || !obj.Entity:IsValid() then
		INV.ResetObject( UID, id )
		return false
	end
	
	-- Store per-object stuff in local vars to save some function calls.
	local model = obj.Entity:GetModel() || "none"
	local name  = obj.Entity:GetName() 	|| "none"
	local physobj = obj.Entity:GetPhysicsObject()
	if !physobj || !physobj:IsValid() then
		return false 
	end
	local weight = physobj:GetMass()
	
	-- Save everything we want the client to know.
	obj.InUse 	= true
	obj.Name  	= name
	obj.Model 	= model
	obj.Weight	= weight
	
	-- "Remove" the entity from the world now we have it stored.
	ent:SetNoDraw( true )
	ent:SetNotSolid( true )	
	ent:SetPos( ply:GetPos() )
	ent:SetParent( ply )
	
	-- Finally update the client.
	INV.UpdateClient( UID, id )
	return true
end 

-- Function to call the INV.PickUp function when 'e' is pressed.
function INV.KeyPress( ply, key )
	UID = ply:UniqueID()
	if (key == IN_USE) then
		if (INV[UID].Held == nil) then
			local trace = util.GetPlayerTrace(ply)
			local tr = util.TraceLine(trace)

			if (trace.start - tr.HitPos):Length() > 100 then
				return
			end

			if tr.HitWorld then
			elseif tr.HitNonWorld and tr.MatType == MAT_GLASS then
			elseif tr.Entity:IsPlayer() then
			else
				local physobj = tr.Entity:GetPhysicsObject()
				if physobj:IsValid() and tr.Entity:GetClass() != "prop_door_rotating" then
					if /*physobj:GetMass() <= 370 and */ /*(!tr.Entity:IsConstrained() or tr.Entity:GetName() == "__BARRICADE__"  or tr.Entity:IsParent()) and */ !tr.Entity:IsPlayer() then
						constraint.RemoveAll(tr.Entity)
						INV[UID].Held = tr.Entity
						//print(INV[UID].Held:OnGround())
						INV[UID].Held:SetNetworkedBool("heldprop",true)
						INV[UID].Held:SetColor(255,255,255,128)
						INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_NONE)
						INV[UID].Held:SetNotSolid(true)
						INV[UID].Held:GetPhysicsObject():ApplyForceCenter(ply:GetActiveWeapon().Owner:GetAimVector() * 666)
						INV[UID].HeldAngle = Angle(0,0,0)
						ply:GetActiveWeapon():SendWeaponAnim( ACT_VM_HOLSTER )
						ply:DrawViewModel(false)
						print(INV[UID].Held:Health() .. "/" .. INV[UID].Held:GetMaxHealth())
					end
				end
			end
		else
			INV[UID].Held:GetPhysicsObject():ApplyForceCenter(Vector(0,0,0))
			INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE)
			INV[UID].Held:SetColor(255,255,255,255)
			INV[UID].Held:GetPhysicsObject():Wake()
			if (INV.PickUp(ply, INV[UID].Held )) then
				INV[UID].Held:SetNotSolid(true)
				INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_NONE)
			else
				INV[UID].Held:SetNotSolid(false)
				INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE)
				INV[UID].Held:Activate()
			end
			INV[UID].Held:SetNetworkedBool("heldprop",false)
			//INV[UID].Held:SetNotSolid(true)
			INV[UID].Held = nil
			ply:GetActiveWeapon():SendWeaponAnim( ACT_VM_DRAW )
			ply:DrawViewModel(true)
		end
	end
	if (key == IN_RELOAD and INV[UID].Held != nil) then
		local pos = ply:GetShootPos()
		local ang = ply:GetAimVector()
		local tracedata = {}
		INV[UID].Held:GetPhysicsObject():ApplyForceCenter(Vector(0,0,0))
		INV[UID].Held:SetNetworkedBool("heldprop",false)
		INV[UID].Held:SetColor(255,255,255,255)
		INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE)
		INV[UID].Held:SetName("__BARRICADE__")
		constraint.Weld(INV[UID].Held,INV[UID].HeldOther,0,0,0,1)
		INV[UID].Held:SetNotSolid(false)
		INV[UID].Held = nil
		ply:GetActiveWeapon():SendWeaponAnim( ACT_VM_DRAW )
		ply:DrawViewModel(true)
		ply:GetActiveWeapon():SetNetworkedBool("canreload",false)
	end
	if (key == IN_ATTACK and INV[UID].Held != nil) then
		local temp = INV[UID].HeldAngle.r+90
		if (temp > 360) then temp = temp - 360 end
		if (temp < 0) then temp = temp + 360 end
		INV[UID].HeldAngle = Angle(0,0,temp)
	end
	if (key == IN_ATTACK2 and INV[UID].Held != nil) then
		local rpos = INV[UID].Held:GetPos()
		if not util.IsInWorld(rpos) then
			local tra3 = util.GetPlayerTrace(ply)
			local tr3 = util.TraceLine(tra3)
			INV[UID].Held:SetPos(tr3.HitPos)
		end
		
		INV[UID].Held:GetPhysicsObject():ApplyForceCenter(Vector(0,0,0))
		INV[UID].Held:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE)
		INV[UID].Held:SetColor(255,255,255,255)
		INV[UID].Held:SetNetworkedBool("heldprop",false)
		INV[UID].Held:SetNotSolid(false)
		INV[UID].Held = nil
		ply:GetActiveWeapon():SendWeaponAnim( ACT_VM_DRAW )
		ply:DrawViewModel(true)
		ply:GetActiveWeapon():SetNetworkedBool("cansecondary",false)
	end
end
hook.Add( "KeyPress", "INV.KeyPress", INV.KeyPress )

function INV.KeyRelease(ply,key)
	if (key == IN_RELOAD) then
		ply:GetActiveWeapon():SetNetworkedBool("canreload",true)
	end
	if (key == IN_ATTACK2) then
		ply:GetActiveWeapon():SetNetworkedBool("cansecondary",true)
	end
end
hook.Add("KeyRelease","INV.KeyRelease",INV.KeyRelease)

-- Function to drop an "Object".
function INV.DropObject( ply, id )
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.
	
	local obj = INV[UID].Objects[id] -- Store a quick reference to the object we want to drop.
	
	-- Early out if the entity does not exist. TODO: Remove the object from the list if it is removed while in the inventory. eg. bomb exploding.
	-- Also delete object.
	if !obj.Entity || !obj.Entity:IsValid() then
		-- Reset slots, objects and update the client all in one go!
		INV.ResetObject( UID, id )
		INV.UpdateClient( UID, id )
		return false 
	end 
	
	-- Create another reference to our entity.
	local newent = obj.Entity

	-- Return it to it's former glory.
	newent:SetParent()
	
	newent:SetAngles ( ply:EyeAngles() )
	newent:SetPos ( ply:EyePos() + ( ply:GetAimVector() * 50) )
	newent:SetNoDraw( false )
	newent:Spawn()
	newent:Activate()
	newent:SetCollisionGroup(COLLISION_GROUP_INTERACTIVE)
	newent:GetPhysicsObject():Wake()
	newent:SetNotSolid( false )
	
	INV.ResetObject( UID, id )
	INV.UpdateClient( UID, id )
end

function INV.Drop( ply, command, args )
	local id = tonumber(args[1])
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.
	local obj = INV[UID].Objects[id]
	
	if !obj || !obj.InUse then return false end -- Early out if we get a dud object. 
	
	INV.DropObject( ply, id )
	return true			
end
concommand.Add( "~INV_DROP", INV.Drop )

-- Function to USE a stored entity.
function INV.UseObject( UID, id )	
	local obj = INV[UID].Objects[id] -- Store a quick reference to the object we want to use.
	
	-- If the object is not valid then kill it.
	if !obj || !obj.Entity || !obj.Entity:IsValid() then
		INV.ResetObject( UID, id )
		INV.UpdateClient( UID, id )
		return false 
	end
	
	--  If the object does not have a Use function then don't continue - we probably want to tell the client this...
	if !obj.Entity.Use then return false end 
	
	-- What the hell is caller!?
	local activator = player.GetByID(UID) -- hehehe
	local caller = GetWorldEntity()
	
	-- Use the item and store what it returns so we can decide how to procede.
	local ret = obj.Entity:Use( activator, caller )
	
	-- If the entity has mysteriously dissapeared during the "use" then remove the object and return outta here.
	if !obj.Entity || !obj.Entity:IsValid() then
		INV.ResetObject( UID, id )
		INV.UpdateClient( UID, id )
		return true
	end
	
	-- If the entity is still here, check what it returned.
	-- If anything except "keepme" was returned, remove the entity
	if ret == "keepme" then
		-- Um, don't do anything? Once again we prob want to tell the client what happened.
		return true
	end
	
	INV.ResetObject( UID, id )
	INV.UpdateClient( UID, id )
	
	return true
end

-- Function to drop the object contained in an inventory "slot".
function INV.Use( ply, command, args )
	local id = tonumber(args[1])
	local UID = ply:UniqueID() -- store the unique ID to save excess function calls.
	local obj = INV[UID].Objects[id]
	
	if !obj || !obj.InUse then return false end -- Early out if we get a dud object. 
	
	INV.UseObject( UID, id )
	return true	
end
concommand.Add( "~INV_USE", INV.Use )

function INV.DropAll( ply, command, args )
	for i = 1, INV.Conf.NumSlots do
		ply:ConCommand( "~INV_DROP "..i.."\n" )
	end
end
concommand.Add( "INV_dropall", INV.DropAll )

function INV.UseAll( ply, command, args )
	for i = 1, INV.Conf.NumSlots do
		ply:ConCommand( "~INV_USE "..i.."\n" )
	end
end
concommand.Add( "INV_useall", INV.UseAll )

function INV.PropDamage(ent,inflictor,attacker,amount,dmginfo)
	if ( ent:GetClass() == "prop_physics" or ent:GetClass() == "prop_physics_multiplayer") then
		local hp = ent:Health()
		if ( hp - amount <= 0 ) then
			// break
			ent:GibBreakClient(ent:GetPos())
			ent:Remove()
			return true
		else
			ent:SetHealth( hp - amount )
			return true
		end
	end
end
hook.Add( "EntityTakeDamage", "INV.PropDamage", INV.PropDamage )

function INV.Think()
	for n,ply in pairs(player.GetAll()) do
		UID = ply:UniqueID()
		INV[UID].HeldOther = nil
		if INV[UID].Held != nil then
			if !INV[UID].Held:IsValid() or !INV[UID].Held:GetPhysicsObject():IsValid() or !INV[UID].Held then
				INV[UID].Held = nil
			elseif INV[UID].Held then
				local pos = ply:GetShootPos() + Vector(0, 0, -15)
				local ang = ply:GetAimVector() + Vector(0, 0, 0.13) 
				local dmin = INV[UID].Held:OBBMins()
				local dmax = INV[UID].Held:OBBMaxs()
				local size = dmin:Distance(dmax)
				
				INV[UID].Held:SetColor(255,255,255,127)
				
				local trace9 = util.GetPlayerTrace(ply)
				trace9.filter = {INV[UID].Held, ply}
				local tr9 = util.TraceLine(trace9)
				if (trace9.start - tr9.HitPos):Length() < 60 then
					if tr9.HitWorld or (tr9.HitNonWorld and tr9.Entity and tr9.Entity:IsValid() and string.find(tr9.Entity:GetClass(),"prop") and !string.find(tr9.Entity:GetClass(),"rag")) then
						local tr = util.GetPlayerTrace( ply, ply:GetCursorAimVector() ) 
						tr.filter = {INV[UID].Held, ply}
						local trace = util.TraceLine( tr ) 
						if (trace.Hit) then
							INV[UID].Held:SetColor(255,255,255,230)
							INV[UID].Held:SetAngles((trace.HitNormal * -1):Angle() + INV[UID].HeldAngle)
							INV[UID].Held:SetPos(trace.HitPos)						
							INV[UID].Held:GetPhysicsObject():SetVelocityInstantaneous(Vector(0,0,0))
							INV[UID].Held:SetLocalVelocity(Vector(0,0,0))
							if (trace.Entity:IsWorld()) then
								INV[UID].HeldOther = GetWorldEntity()
							else
								INV[UID].HeldOther = trace.Entity
							end
						end
					end
				else
					INV[UID].Held:SetPos(pos + (ang * (INV[UID].Held:BoundingRadius() * 1.45)))
					INV[UID].Held:SetAngles(ply:GetForward():Normalize():Angle() + INV[UID].HeldAngle)
				end
			end
		end
		ply:GetActiveWeapon():SetNetworkedEntity("held",INV[UID].Held)
		ply:GetActiveWeapon():SetNetworkedEntity("other",INV[UID].HeldOther)
	end
end
hook.Add("Think","INV.Think",INV.Think)
